from examples.example_6_simplest_case_storewrapper.ex_6_model_classes_with_vidigi_logging import Trial, g
from vidigi.animation import animate_activity_log
import pandas as pd
import plotly.io as pio
pio.renderers.default = "notebook"
import osFeature Breakdown: Changing how the date and time are displayed
We can change the start time of our simulation to best reflect our simulation’s parameters.
SIM_START_TIME = "08:00:00"import random
import numpy as np
import pandas as pd
import simpy
from sim_tools.distributions import Exponential, Lognormal
from vidigi.resources import VidigiStore
# Class to store global parameter values. We don't create an instance of this
# class - we just refer to the class blueprint itself to access the numbers
# inside.
class g:
'''
Create a scenario to parameterise the simulation model
Parameters:
-----------
random_number_set: int, optional (default=DEFAULT_RNG_SET)
Set to control the initial seeds of each stream of pseudo
random numbers used in the model.
n_cubicles: int
The number of treatment cubicles
trauma_treat_mean: float
Mean of the trauma cubicle treatment distribution (Lognormal)
trauma_treat_var: float
Variance of the trauma cubicle treatment distribution (Lognormal)
arrival_rate: float
Set the mean of the exponential distribution that is used to sample the
inter-arrival time of patients
sim_duration: int
The number of time units the simulation will run for
number_of_runs: int
The number of times the simulation will be run with different random number streams
'''
random_number_set = 42
n_cubicles = 4
trauma_treat_mean = 40
trauma_treat_var = 5
arrival_rate = 5
sim_duration = 600
number_of_runs = 100
# Class representing patients coming in to the clinic.
class Patient:
'''
Class defining details for a patient entity
'''
def __init__(self, p_id):
'''
Constructor method
Params:
-----
identifier: int
a numeric identifier for the patient.
'''
self.identifier = p_id
self.arrival = -np.inf
self.wait_treat = -np.inf
self.total_time = -np.inf
self.treat_duration = -np.inf
# Class representing our model of the clinic.
class Model:
'''
Simulates the simplest minor treatment process for a patient
1. Arrive
2. Examined/treated by nurse when one available
3. Discharged
'''
# Constructor to set up the model for a run. We pass in a run number when
# we create a new model.
def __init__(self, run_number):
# Create a SimPy environment in which everything will live
self.env = simpy.Environment()
self.event_log = []
# Create a patient counter (which we'll use as a patient ID)
self.patient_counter = 0
self.patients = []
# Create our resources
self.init_resources()
# Store the passed in run number
self.run_number = run_number
# Create a new Pandas DataFrame that will store some results against
# the patient ID (which we'll use as the index).
self.results_df = pd.DataFrame()
self.results_df["Patient ID"] = [1]
self.results_df["Queue Time Cubicle"] = [0.0]
self.results_df["Time with Nurse"] = [0.0]
self.results_df.set_index("Patient ID", inplace=True)
# Create an attribute to store the mean queuing times across this run of
# the model
self.mean_q_time_cubicle = 0
self.patient_inter_arrival_dist = Exponential(mean = g.arrival_rate,
random_seed = self.run_number*g.random_number_set)
self.treat_dist = Lognormal(mean = g.trauma_treat_mean,
stdev = g.trauma_treat_var,
random_seed = self.run_number*g.random_number_set)
def init_resources(self):
'''
Init the number of resources
and store in the arguments container object
Resource list:
1. Nurses/treatment bays (same thing in this model)
'''
self.treatment_cubicles = VidigiStore(self.env, num_resources=g.n_cubicles)
# A generator function that represents the DES generator for patient
# arrivals
def generator_patient_arrivals(self):
# We use an infinite loop here to keep doing this indefinitely whilst
# the simulation runs
while True:
# Increment the patient counter by 1 (this means our first patient
# will have an ID of 1)
self.patient_counter += 1
# Create a new patient - an instance of the Patient Class we
# defined above. Remember, we pass in the ID when creating a
# patient - so here we pass the patient counter to use as the ID.
p = Patient(self.patient_counter)
# Store patient in list for later easy access
self.patients.append(p)
# Tell SimPy to start up the attend_clinic generator function with
# this patient (the generator function that will model the
# patient's journey through the system)
self.env.process(self.attend_clinic(p))
# Randomly sample the time to the next patient arriving. Here, we
# sample from an exponential distribution (common for inter-arrival
# times), and pass in a lambda value of 1 / mean. The mean
# inter-arrival time is stored in the g class.
sampled_inter = self.patient_inter_arrival_dist.sample()
# Freeze this instance of this function in place until the
# inter-arrival time we sampled above has elapsed. Note - time in
# SimPy progresses in "Time Units", which can represent anything
# you like (just make sure you're consistent within the model)
yield self.env.timeout(sampled_inter)
# A generator function that represents the pathway for a patient going
# through the clinic.
# The patient object is passed in to the generator function so we can
# extract information from / record information to it
def attend_clinic(self, patient):
self.arrival = self.env.now
self.event_log.append(
{'patient': patient.identifier,
'pathway': 'Simplest',
'event_type': 'arrival_departure',
'event': 'arrival',
'time': self.env.now}
)
# request examination resource
start_wait = self.env.now
self.event_log.append(
{'patient': patient.identifier,
'pathway': 'Simplest',
'event': 'treatment_wait_begins',
'event_type': 'queue',
'time': self.env.now}
)
# Seize a treatment resource when available
with self.treatment_cubicles.request() as req:
treatment_resource = yield req
# record the waiting time for registration
self.wait_treat = self.env.now - start_wait
self.event_log.append(
{'patient': patient.identifier,
'pathway': 'Simplest',
'event': 'treatment_begins',
'event_type': 'resource_use',
'time': self.env.now,
'resource_id': treatment_resource.id_attribute
}
)
# sample treatment duration
self.treat_duration = self.treat_dist.sample()
yield self.env.timeout(self.treat_duration)
self.event_log.append(
{'patient': patient.identifier,
'pathway': 'Simplest',
'event': 'treatment_complete',
'event_type': 'resource_use_end',
'time': self.env.now,
'resource_id': treatment_resource.id_attribute}
)
# total time in system
self.total_time = self.env.now - self.arrival
self.event_log.append(
{'patient': patient.identifier,
'pathway': 'Simplest',
'event': 'depart',
'event_type': 'arrival_departure',
'time': self.env.now}
)
# This method calculates results over a single run. Here we just calculate
# a mean, but in real world models you'd probably want to calculate more.
def calculate_run_results(self):
# Take the mean of the queuing times across patients in this run of the
# model.
self.mean_q_time_cubicle = self.results_df["Queue Time Cubicle"].mean()
# The run method starts up the DES entity generators, runs the simulation,
# and in turns calls anything we need to generate results for the run
def run(self):
# Start up our DES entity generators that create new patients. We've
# only got one in this model, but we'd need to do this for each one if
# we had multiple generators.
self.env.process(self.generator_patient_arrivals())
# Run the model for the duration specified in g class
self.env.run(until=g.sim_duration)
# Now the simulation run has finished, call the method that calculates
# run results
self.calculate_run_results()
self.event_log = pd.DataFrame(self.event_log)
self.event_log["run"] = self.run_number
return {'results': self.results_df, 'event_log': self.event_log}
# Class representing a Trial for our simulation - a batch of simulation runs.
class Trial:
# The constructor sets up a pandas dataframe that will store the key
# results from each run against run number, with run number as the index.
def __init__(self):
self.df_trial_results = pd.DataFrame()
self.df_trial_results["Run Number"] = [0]
self.df_trial_results["Arrivals"] = [0]
self.df_trial_results["Mean Queue Time Cubicle"] = [0.0]
self.df_trial_results.set_index("Run Number", inplace=True)
self.all_event_logs = []
# Method to run a trial
def run_trial(self):
print(f"{g.n_cubicles} nurses")
print("") ## Print a blank line
# Run the simulation for the number of runs specified in g class.
# For each run, we create a new instance of the Model class and call its
# run method, which sets everything else in motion. Once the run has
# completed, we grab out the stored run results (just mean queuing time
# here) and store it against the run number in the trial results
# dataframe.
for run in range(g.number_of_runs):
random.seed(run)
my_model = Model(run)
model_outputs = my_model.run()
patient_level_results = model_outputs["results"]
event_log = model_outputs["event_log"]
self.df_trial_results.loc[run] = [
len(patient_level_results),
my_model.mean_q_time_cubicle,
]
# print(event_log)
self.all_event_logs.append(event_log)
self.all_event_logs = pd.concat(self.all_event_logs)my_trial = Trial()
my_trial.run_trial()4 nurses
SIM_START_DATE = "2024-01-01"
STEP_SNAPSHOT_MAX = 45
LIMIT_DURATION = g.sim_duration
WRAP_QUEUES_AT = 15event_position_df = pd.DataFrame([
{'event': 'arrival',
'x': 50, 'y': 300,
'label': "Arrival" },
# Triage - minor and trauma
{'event': 'treatment_wait_begins',
'x': 205, 'y': 275,
'label': "Waiting for Treatment"},
{'event': 'treatment_begins',
'x': 205, 'y': 175,
'resource':'n_cubicles',
'label': "Being Treated"},
{'event': 'exit',
'x': 270, 'y': 70,
'label': "Exit"}
])Run using the all-in-one animation function
Let’s first look at the output if we don’t use the start date or ask for the data to displayed in days, hours and minutes.
animate_activity_log(
event_log=my_trial.all_event_logs[my_trial.all_event_logs['run']==1],
event_position_df= event_position_df,
scenario=g(),
entity_col_name="patient",
every_x_time_units=1,
gap_between_entities=6,
plotly_height=700,
frame_duration=200,
plotly_width=1200,
override_x_max=300,
override_y_max=500,
limit_duration=g.sim_duration,
wrap_queues_at=25,
step_snapshot_max=125,
display_stage_labels=False,
add_background_image="https://raw.githubusercontent.com/Bergam0t/vidigi/refs/heads/main/examples/example_1_simplest_case/Simplest%20Model%20Background%20Image%20-%20Horizontal%20Layout.drawio.png",
)Next, let’s see what happens when we request it in days, hours and minutes using the time_display_units parameter, but don’t specify a simulation start date.
animate_activity_log(
time_display_units="dhm",
event_log=my_trial.all_event_logs[my_trial.all_event_logs['run']==1],
event_position_df= event_position_df,
entity_col_name="patient",
scenario=g(),
every_x_time_units=1,
gap_between_entities=6,
plotly_height=700,
frame_duration=200,
plotly_width=1200,
override_x_max=300,
override_y_max=500,
limit_duration=g.sim_duration,
wrap_queues_at=25,
step_snapshot_max=125,
display_stage_labels=False,
add_background_image="https://raw.githubusercontent.com/Bergam0t/vidigi/refs/heads/main/examples/example_1_simplest_case/Simplest%20Model%20Background%20Image%20-%20Horizontal%20Layout.drawio.png",
)animate_activity_log(
event_log=my_trial.all_event_logs[my_trial.all_event_logs['run']==1],
event_position_df= event_position_df,
scenario=g(),
entity_col_name="patient",
every_x_time_units=1,
include_play_button=True,
gap_between_entities=6,
plotly_height=700,
frame_duration=200,
plotly_width=1200,
override_x_max=300,
override_y_max=500,
limit_duration=g.sim_duration,
wrap_queues_at=25,
step_snapshot_max=125,
time_display_units="dhm",
start_date=SIM_START_DATE,
display_stage_labels=False,
add_background_image="https://raw.githubusercontent.com/Bergam0t/vidigi/refs/heads/main/examples/example_1_simplest_case/Simplest%20Model%20Background%20Image%20-%20Horizontal%20Layout.drawio.png",
)Finally, we’ll provide a start date for the simulation to display.
animate_activity_log(
entity_col_name="patient",
time_display_units="dhm",
start_date=SIM_START_DATE,
start_time=SIM_START_TIME,
event_log=my_trial.all_event_logs[my_trial.all_event_logs['run']==1],
event_position_df= event_position_df,
scenario=g(),
every_x_time_units=1,
gap_between_entities=6,
plotly_height=700,
frame_duration=200,
plotly_width=1200,
override_x_max=300,
override_y_max=500,
limit_duration=g.sim_duration,
wrap_queues_at=25,
step_snapshot_max=125,
display_stage_labels=False,
add_background_image="https://raw.githubusercontent.com/Bergam0t/vidigi/refs/heads/main/examples/example_1_simplest_case/Simplest%20Model%20Background%20Image%20-%20Horizontal%20Layout.drawio.png",
)Changing the timescale
We could also display the simulation at a daily level. Let’s imagine our simulation represents the number of patients being seen per day instead of per minute.
NOTE: These example animations won’t make a lot of sense as the underlying simulation doesn’t really make sense at a higher resolution than daily! but it gives you some idea of how these parameters can change and their impact on how dates and times are displayed.
animate_activity_log(
simulation_time_unit="days",
time_display_units="d",
start_date=SIM_START_DATE,
entity_col_name="patient",
event_log=my_trial.all_event_logs[my_trial.all_event_logs['run']==1],
event_position_df= event_position_df,
scenario=g(),
every_x_time_units=1,
gap_between_entities=6,
plotly_height=700,
frame_duration=2000,
frame_transition_duration=1000,
plotly_width=1200,
override_x_max=300,
override_y_max=500,
limit_duration=g.sim_duration,
wrap_queues_at=25,
step_snapshot_max=200,
display_stage_labels=False,
add_background_image="https://raw.githubusercontent.com/Bergam0t/vidigi/refs/heads/main/examples/example_1_simplest_case/Simplest%20Model%20Background%20Image%20-%20Horizontal%20Layout.drawio.png",
)Or per month…
animate_activity_log(
simulation_time_unit="months",
time_display_units="m",
entity_col_name="patient",
start_date=SIM_START_DATE,
start_time=SIM_START_TIME,
event_log=my_trial.all_event_logs[my_trial.all_event_logs['run']==1],
event_position_df=event_position_df,
scenario=g(),
every_x_time_units=1, # Every day
gap_between_entities=6,
plotly_height=700,
frame_duration=1000,
frame_transition_duration=1000,
plotly_width=1200,
override_x_max=300,
override_y_max=500,
limit_duration=g.sim_duration,
wrap_queues_at=25,
step_snapshot_max=1000,
display_stage_labels=False,
add_background_image="https://raw.githubusercontent.com/Bergam0t/vidigi/refs/heads/main/examples/example_1_simplest_case/Simplest%20Model%20Background%20Image%20-%20Horizontal%20Layout.drawio.png",
)